text: # -*- coding: utf-8 -*-
  #
  # Copyright (c) 2016 Muges
  #
  # Permission is hereby granted, free of charge, to any person
  # obtaining a copy of this software and associated documentation files
  # (the "Software"), to deal in the Software without restriction,
  # including without limitation the rights to use, copy, modify, merge,
  # publish, distribute, sublicense, and/or sell copies of the Software,
  # and to permit persons to whom the Software is furnished to do so,
  # subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be
  # included in all copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  # SOFTWARE.
  
  """
  A model used to store podcasts in the library.
  """
  
  import logging
  import os.path
  import requests
  from peewee import BooleanField, TextField, DoesNotExist, IntegrityError, fn
  
  from erika import parsers
  from erika.library.fields import Image, ImageField
  from erika.util import sanitize_filename
  from .database import BaseModel, database, slugify
  from .config import Config
  from .episode import Episode
  from .episode import Podcast as PodcastProxy
  from .podcast_action import PodcastAction
  
  
  class Podcast(BaseModel):
      """A model used to store podcasts in the library.
  
      Attributes
      ----------
      parser : str
          The name of the parser (see :mod:`erika.parsers`).
      url : str
          The url of the podcast.
      title : str, optional
          The title of the podcast.
      author : str, optional
          The author of the podcast.
      image_url : str, optional
          A link to the image of the podcast
      image : :class:`~erika.library.fields.Image`, optional
          The image of the podcast.
      language : str, optional
          The language of the podcast.
      subtitle : str, optional
          The subtitle of the podcast.
      summary : str, optional
          A description of the podcast.
      link : str, optional
          Website link.
      update_failed : bool
          True if the last update of the podcast failed.
      """
      parser = TextField()
      url = TextField()
  
      title = TextField(null=True)
      author = TextField(null=True)
      image_url = TextField(null=True)
      image = ImageField(null=True)
      language = TextField(null=True)
      subtitle = TextField(null=True)
      summary = TextField(null=True)
      link = TextField(null=True)
  
      update_failed = BooleanField(default=False)
  
      class Meta:  # pylint: disable=too-few-public-methods, missing-docstring
          indexes = (
              (('parser', 'url'), True),
          )
  
      @property
      def display_subtitle(self):
          """str: The subtitle of the podcast, or its summary if it has no
          subtitle."""
          return self.subtitle or self.summary or ""
  
      @property
      def display_summary(self):
          """str: The summary of the podcast, or its subtitle if it has no
          summary."""
          return self.summary or self.subtitle or ""
  
      @property
      def display_title(self):
          """str: The title of the podcast, or its url if it has no title."""
          return self.title or self.url or ""
  
      @property
      def episodes_count(self):
          """int: the number of episodes of the podcast."""
          return self.episodes.count()
  
      @property
      def played_count(self):
          """int: the number of episodes that have already been played."""
          # pylint: disable=singleton-comparison
          return self.episodes.where(Episode.played == True).count()
  
      @property
      def new_count(self):
          """int: the number of episodes that have been added since the previous
          launch."""
          # pylint: disable=singleton-comparison
          return self.episodes.where(Episode.new == True).count()
  
      @property
      def unplayed_count(self):
          """int: the number of episodes that have not been played."""
          return self.episodes_count - self.played_count
  
      @classmethod
      def new(cls, parser, url):
          """Create a new podcast, and add it to the database
  
          Parameters
          ----------
          parser_name : str
              The name of a parser (see :mod:`erika.parsers`).
          url : str
              The url of the podcast.
          """
          logger = logging.getLogger(".".join((__name__, cls.__name__)))
          logger.info("Adding the %s podcast %s.", parser, url)
          try:
              podcast = Podcast.get(parser=parser, url=url)
          except DoesNotExist:
              podcast = Podcast(parser=parser, url=url)
              podcast.save()
  
              if podcast.parser == 'rss':
                  PodcastAction.new(podcast_url=podcast.url, action="add")
          else:
              logger.warning("The %s podcast %s is already in the database.",
                             parser, url)
  
          return podcast
  
      def delete_instance(self, *args, **kwargs):
          # pylint: disable=arguments-differ
          PodcastAction.new(podcast_url=self.url, action="remove")
          super().delete_instance(*args, **kwargs)
  
      def update_podcast(self):
          """Update the podcast."""
          logger = logging.getLogger(
              ".".join((__name__, self.__class__.__name__)))
          logger.info("Updating the podcast %s.", self.display_title)
  
          try:
              # Parse the podcast
              episodes = parsers.parse(self)
  
              # Download the podcast's image if its url changed
              dirty_fields = [field.name for field in self.dirty_fields]
              if 'image_url' in dirty_fields:
                  self.download_image()
  
              # Get the track number that should be used for the next new episode
              next_track_number = self.get_next_track_number()
  
              # Save the podcast and the episodes in the database
              with database.transaction():
                  self.update_failed = False
                  self.save()
  
                  for episode in episodes:
                      episode.track_number = next_track_number
                      try:
                          episode.save()
                      except IntegrityError:
                          # The episode is already in the database, don't
                          # increment the track number.
                          continue
                      next_track_number += 1
          except Exception:  # pylint: disable=broad-except
              logger.exception("Unable to update the podcast.")
  
              self.update_failed = True
              self.save()
  
      def download_image(self):
          """Download the podcast's image"""
          logger = logging.getLogger(
              ".".join((__name__, self.__class__.__name__)))
          logger.debug("Downloading image for the podcast %s.", self.title)
          try:
              response = requests.get(self.image_url)
              response.raise_for_status()
          except requests.exceptions.RequestException:
              logger.exception("Unable to download the image.")
              return
  
          self.image = Image(response.content)
  
      def get_next_track_number(self):
          """Get the track number of the next episode."""
          previous = self.episodes.select(fn.Max(Episode.track_number)).get()
          previous_number = previous.track_number or 0
          return previous_number + 1
  
      def get_directory(self):
          """Return the directory containing a podcast's audio files.
  
          Returns
          -------
          str
              The path of the directory.
          """
          template = Config.get_value("library.podcast_directory_template")
          return os.path.join(
              Config.get_value("library.root"),
              sanitize_filename(template.format(podcast=self))
          )
  
      def get_episode_from_tags_strict(self, audiotags):
          """Get the podcast's episode matching a file strictly.
  
          Returns the episode having the same guid, or the same title
          and publication date if there is one (meaning it was probably
          tagged by erika).
  
          Parameters
          ----------
          audiotags : dict
              Dictionnary containing the audio file's metadata.
  
          Returns
          -------
          :class:`Episode`, optional
              The episode, or None if there was no match.
          """
          logger = logging.getLogger(
              ".".join((__name__, self.__class__.__name__)))
  
          guid = audiotags['guid']
          pubdate = audiotags['pubdate']
          title = audiotags['title']
  
          try:
              episode = self.episodes.where(
                  Episode.local_path.is_null(),
                  (Episode.guid == guid) |
                  ((Episode.pubdate == pubdate) & (Episode.title == title))
              ).first()
          except DoesNotExist:
              logger.debug("No strict match.")
              return None
  
          return episode
  
      def get_episode_from_tags_loose(self, audiotags, filename):
          """Get the podcast's episode matching a file loosely.
  
          Returns the episode having a title similar to the title or
          filename of the audio file. If there is more than one such
          match, None is returned as there is no way to find which
          episode is the right one.
  
          Parameters
          ----------
          audiotags : dict
              Dictionnary containing the audio file's metadata.
          filename : str
              The name of the audio file (without extensions or directory).
  
          Returns
          -------
          :class:`Episode`, optional
              The episode, or None if there was no match.
          """
          logger = logging.getLogger(
              ".".join((__name__, self.__class__.__name__)))
  
          title = audiotags['title']
  
          # Try to find a single episode with the same title
          episodes = list(self.episodes.where(
              Episode.local_path.is_null(),
              Episode.title == title).limit(2))
          if episodes:
              if len(episodes) > 1:
                  logger.warning("Too many episodes with title %s.", title)
                  return None
  
              return episodes[0]
  
          # Try to find a single episode with a title or filename similar to the
          # title
          filename = slugify(filename)
          title = slugify(title or "")
  
          episodes = list(self.episodes.where(
              Episode.local_path.is_null(),
              fn.slugify(Episode.title) << [title, filename]).limit(2))
          if episodes:
              if len(episodes) > 1:
                  logger.warning("Too many episodes with title similar to '%s' "
                                 "or '%s'.", title, filename)
                  return None
  
              return episodes[0]
  
  
  PodcastProxy.initialize(Podcast)
  